using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

namespace Polymod.TemplateBuilders
{
    public class ModelTemplateBuilder : IModelTemplateBuilder, IRegisterInvoker
    {
        private readonly object _syncLock = new object();
        private IModelFactory _modelFactory;
        private readonly Dictionary<Type, InvokerRegistry> _invokers = new Dictionary<Type, InvokerRegistry>();

        /// <summary>
        /// Gets ModelFactory
        /// </summary>
        protected IModelFactory ModelFactory
        {
            get { return _modelFactory; }
        }


        private Type BuildModelType(ModelTemplateParameters parameters)
        {
            var moduleBuilder = parameters.ModuleBuilder;
            var typeBuilder = moduleBuilder.DefineType(parameters.ModelType.Name, TypeAttributes.Class | TypeAttributes.Public, GetAndValidateModelType(parameters));

            foreach (var p in parameters.ModelType.GetProperties())
            {
                var invoker = CreateInvoker(p);
                SetInvoker(parameters.ModelType, invoker.Name, invoker);
            }
            foreach (var f in parameters.ModelType.GetFields())
            {
                var invoker = CreateInvoker(f);
                SetInvoker(parameters.ModelType, invoker.Name, invoker);
            }

            foreach (var invoker in GetInvokers(parameters.ModelType))
            {
                BuildGetterSetter(invoker.Name, invoker.ReturnType, !invoker.IsReadonly, typeBuilder);
            }

            BuildConstructor(typeBuilder);

            return typeBuilder.CreateType();
        }

        IInvoker IRegisterInvoker.GetInvoker(Type modelType, string name)
        {
            InvokerRegistry invokersForType;
            if (!_invokers.TryGetValue(modelType, out invokersForType)) throw new KeyNotFoundException();
            return invokersForType.GetInvoker(name);

        }


        private IEnumerable<IInvoker> GetInvokers(Type modelType)
        {
            InvokerRegistry registry;
            if (_invokers.TryGetValue(modelType, out registry))
                return registry;
            else
                return new IInvoker[0];
        }

        private Type BuildEnumerableModelType(ModelTemplateParameters parameters)
        {
            var enumerableType = parameters.ModelType.GetInterfaces().First(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>));
            var result = typeof(ObservableCollectionModel<>).MakeGenericType(enumerableType.GetGenericArguments()[0]);

            this.SetInvoker(parameters.ModelType, "Count", CreateInvoker(result.GetProperty("Count")));
            return result;
        }

        private void BuildConstructor(TypeBuilder typeBuilder)
        {
            var constructorSignature = new Type[] { };
            var constructor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis, constructorSignature);
            var constructorIl = constructor.GetILGenerator();
            constructorIl.Emit(OpCodes.Ldarg_0);
            constructorIl.Emit(OpCodes.Call, typeBuilder.BaseType.GetConstructor(constructorSignature)); //Call base constructor
            constructorIl.Emit(OpCodes.Nop);

            //Initialization code goes here...

            constructorIl.Emit(OpCodes.Nop);
            constructorIl.Emit(OpCodes.Ret);

        }

        private void BuildGetterSetter(string name, Type returnType, bool isWriteable, TypeBuilder typeBuilder)
        {
            var propertyBuilder = typeBuilder.DefineProperty(name, PropertyAttributes.None, returnType, null);

            MethodAttributes getSetAttr =
                        MethodAttributes.Public | MethodAttributes.SpecialName |
                            MethodAttributes.HideBySig;

            // Define the "get" accessor method for CustomerName.
            MethodBuilder propertyGetMethodBuilder =
                typeBuilder.DefineMethod("get_" + name,
                                           getSetAttr,
                                           returnType,
                                           Type.EmptyTypes);

            ILGenerator propertyGetIL = propertyGetMethodBuilder.GetILGenerator();

            propertyGetIL.Emit(OpCodes.Nop);
            propertyGetIL.Emit(OpCodes.Ldarg_0);
            propertyGetIL.Emit(OpCodes.Ldstr, name);
            propertyGetIL.Emit(OpCodes.Callvirt, typeof(IModel).GetMethod("Get"));
            propertyGetIL.Emit(OpCodes.Unbox_Any, returnType);
            propertyGetIL.Emit(OpCodes.Ret);
            propertyBuilder.SetGetMethod(propertyGetMethodBuilder);


            if (isWriteable)
            {
                // Define the "set" accessor method for CustomerName.
                MethodBuilder propertySetMethodBuilder =
                    typeBuilder.DefineMethod("set_" + name,
                                               getSetAttr,
                                               null,
                                               new Type[] { returnType });

                ILGenerator propertySetIL = propertySetMethodBuilder.GetILGenerator();

                propertySetIL.Emit(OpCodes.Nop);
                propertySetIL.Emit(OpCodes.Ldarg_0);
                propertySetIL.Emit(OpCodes.Ldstr, name);
                propertySetIL.Emit(OpCodes.Ldarg_1);
                propertySetIL.Emit(OpCodes.Box, returnType);
                propertySetIL.Emit(OpCodes.Callvirt, typeof(IModel).GetMethod("Set"));
                propertySetIL.Emit(OpCodes.Nop);
                propertySetIL.Emit(OpCodes.Ret);

                propertyBuilder.SetSetMethod(propertySetMethodBuilder);
            }
        }

        private Type GetAndValidateModelType(ModelTemplateParameters parameters)
        {
            var result = GetModelBaseType(parameters);
            if (result == null) throw new InvalidOperationException("GetModelBaseType must not return null.");
            if (!typeof(IModel).IsAssignableFrom(result)) throw new InvalidOperationException("GetModelBaseType must return a Model type.");
            if (result.IsSealed) throw new InvalidOperationException("GetModelBaseType must not return sealed type.");

            try
            {
                //Test-drive the new dynamic type.
                Activator.CreateInstance(result);
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(string.Format("Cannot derive from '{0}'. Make sure base class is a valid inheritable class.", result), ex);
            }

            return result;
        }

        protected virtual Type GetModelBaseType(ModelTemplateParameters parameters)
        {
            return typeof(ModelBase<>).MakeGenericType(parameters.ModelType);
        }

        protected virtual IInvoker CreateInvoker(PropertyInfo propertyInfo)
        {
            return ExpressionHelper.CreateInvoker(propertyInfo);
        }

        protected virtual IInvoker CreateInvoker(FieldInfo fieldInfo)
        {
            return ExpressionHelper.CreateInvoker(fieldInfo);
        }

        public IModelTemplate BuildTemplate(ModelTemplateParameters parameters)
        {
            if (_modelFactory == null) _modelFactory = parameters.ModelFactory;
            else if (_modelFactory != parameters.ModelFactory) throw new InvalidOperationException("Cannot use same template from multiple factories.");

            //Test for anything IEnumerable<X>
            if (parameters.ModelType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
            {
                var modelType = BuildEnumerableModelType(parameters);
                var state = CreateModelTemplateState(parameters);
                return new ModelTemplate(modelType, state);
            }
            else
            {
                var modelType = BuildModelType(parameters);
                var state = CreateModelTemplateState(parameters);
                return new ModelTemplate(modelType, state);
            }
        }

        protected virtual IDictionary<string, object> CreateModelTemplateState(ModelTemplateParameters parameters)
        {
            var result = new Dictionary<string, object>();
            result.Add(Constants.InvokerRegistry, _invokers[parameters.ModelType]);
            result.Add(Constants.ModelFactory, parameters.ModelFactory);
            return result;
        }

        protected void SetInvoker(Type modelType, string name, IInvoker invoker)
        {
            InvokerRegistry invokersForType;
            if (!_invokers.TryGetValue(modelType, out invokersForType))
            {
                lock (_syncLock)
                {
                    if (!_invokers.TryGetValue(modelType, out invokersForType))
                    {
                        invokersForType = new InvokerRegistry();
                        _invokers[modelType] = invokersForType;
                    }
                }
            }
            invokersForType.SetInvoker(name, invoker);
        }

        void IRegisterInvoker.SetInvoker(Type modelType, string name, IInvoker invoker)
        {
            SetInvoker(modelType, name, invoker);
        }
    }
}
